#!/usr/bin/env lua
--
-- Copyright (c) 2018-2019, Tano Systems. All Rights Reserved.
-- Anton Kikin <a.kikin@tano-systems.com>
--

local json = require "luci.jsonc"
local fs   = require "nixio.fs"

local sysfs_net_root = "/sys/class/net"

local function sysfs_net_read(ifname, file)
	local v = nil
	if fs.access(sysfs_net_root .. "/%s/%s" % {ifname, file}) then
		v = fs.readfile(sysfs_net_root .. "/%s/%s" % {ifname, file})
		if v then
			v = v:gsub("\n", "")
		end
	end
	return v or ''
end

local function sysfs_net_read_stats(ifname)
	local s = {
		tx_bytes   = tonumber(sysfs_net_read(ifname, "statistics/tx_bytes")) or 0,
		tx_packets = tonumber(sysfs_net_read(ifname, "statistics/tx_packets")) or 0,
		rx_bytes   = tonumber(sysfs_net_read(ifname, "statistics/rx_bytes")) or 0,
		rx_packets = tonumber(sysfs_net_read(ifname, "statistics/rx_packets")) or 0,
	}

	return s
end

local function sysfs_net_read_bridge(ifname)
	local b = { }

	-- Bridge port number
	b["port"] = tonumber(sysfs_net_read(ifname, "brport/port_no")) or 0

	if b["port"] == 0 then
		return nil
	end

	-- Get bridge system interface name
	local b_path = fs.readlink(sysfs_net_root .. "/%s/brport/bridge" % ifname)
	if b_path then
		b["ifname"] = fs.basename(b_path)
	end

	return b
end

local function type_autodetect(ifname)
	local matches = {
		copper = { "^eth%d+", "^en%l%d+%l%d+", "^sw%d+p%d+" },
		usb_rndis = { "^usb%d+" },
		usb_stick = { "^wwan%d+", "^ww%l%d+%l%d+" },
		ppp = { "^ppp%d+" },
		tunnel = { "^tun%d+", "^tap%d+", "^wg%d+" },
		wifi = { "^wlan%d+", "^wl%l%d+%l%d+" }
	}

	local i, t, m

	for t, m in pairs(matches) do
		for i in pairs(m) do
			if ifname:match(m[i]) then
				return t
			end
		end
	end

	-- default type is 'copper'
	return "copper"
end

local function table_copy(t)
	if t == nil then return nil end
	local u = { }
	for k, v in pairs(t) do u[k] = v end
	return setmetatable(u, getmetatable(t))
end

local methods = {
	getPortsInfo = {
		call = function(args)
			local util = require("luci.util")
			local uci  = require("luci.model.uci").cursor()
			local ntm  = require("luci.model.network").init()
			local fwm  = require("luci.model.firewall").init()

			local bit = require("bit")

			local ports = {
				data = {},
				count = 0
			}

			local netlist = {}
			local brlist = {}

			for _, net in ipairs(ntm:get_networks()) do
				local iface = net:get_interface()
				local wifiname = nil

				if iface ~= nil then
					if iface:type() == "wifi" then
						local wifinet = iface:get_wifinet()
						wifiname = wifinet:id()
					end

					local idx
					local dev
					local name = iface:name()
					local fwzone = fwm:get_zone_by_network(net:name())
					local l = netlist

					if not net:is_alias() and iface:is_bridge() then
						l = brlist
					end

					l[name] = { }
					l[name]["netname"] = net:name()
					l[name]["wifiname"] = wifiname
					if fwzone then
						l[name]["fwzone"]       = fwzone:name()
						l[name]["fwzone_sid"]   = fwzone.sid
					end
				end
			end

			uci:foreach("luci_netports", "port",
				function(section)
					if section["disable"] and (section["disable"] == "true"
						or tonumber(section["disable"]) == 1) then
						-- Disabled in config
						return true
					end

					if not fs.access("/sys/class/net/%s/ifindex" % section["ifname"]) then
						-- Invalid or not existent interface name
						return true
					end

					local new_port = { }
					local ifname = section["ifname"]
					local type   = section["type"]

					local knowntypes = {
						"auto",
						"copper",
						"fixed",
						"usb", -- deprecated
						"usb_stick",
						"usb_rndis",
						"usb_2g",
						"usb_3g",
						"usb_4g",
						"usb_wifi",
						"wifi",
						"vpn",
						"tunnel",
						"ppp",
						"gprs",
						"sfp"
					}

					if not util.contains(knowntypes, type) then
						type = "auto"
					end

					if type == "auto" then
						type = type_autodetect(ifname)
					elseif type == "usb" then
						type = "usb_rndis"
					elseif type == "usb_2g" or
						   type == "usb_3g" or
						   type == "usb_4g" then
						type = "usb_stick"
					elseif type == "usb_wifi" then
						type = "wifi"
					elseif type == "vpn" then
						type = "tunnel"
					end

					-- Port config parameters
					new_port["ifname"] = ifname
					new_port["type"]   = type
					new_port["name"]   = section["name"]

					if not new_port["name"] or new_port["name"] == "" then
						new_port["name"] = ifname
					end

					-- General port interface parameters
					new_port["hwaddr"]  = sysfs_net_read(ifname, "address")
					new_port["carrier"] = tonumber(sysfs_net_read(ifname, "carrier")) or 0

					-- unknown, notpresent, down, lowerlayerdown, testing, dormant, up
					new_port["operstate"] = sysfs_net_read(ifname, "operstate")

					-- up or down
					local flags = sysfs_net_read(ifname, "flags")
					if bit.band(tonumber(flags), 1) == 1 then
						new_port["adminstate"] = "up"
					else
						new_port["adminstate"] = "down"
					end

					if new_port["carrier"] > 0 then
						-- full, half
						new_port["duplex"] = sysfs_net_read(ifname, "duplex")

						-- Value is an integer representing the link speed in Mbits/sec
						new_port["speed"] = tonumber(sysfs_net_read(ifname, "speed")) or 0
						if new_port["speed"] < 0 then
							new_port["speed"] = 0
						end
					end

					-- Port interface statistics
					new_port["stats"] = sysfs_net_read_stats(ifname)

					-- Bridge parameters
					new_port["bridge"] = sysfs_net_read_bridge(ifname)

					-- Parameters for/from netifd
					new_port["ntm"] = table_copy(netlist[ifname])

					if new_port["bridge"] then
						new_port["ntm_bridge"] = table_copy(brlist[new_port["bridge"].ifname])
					end

					new_port["id"] = section[".name"]
					ports.data[#ports.data + 1] = new_port
					ports.count = ports.count + 1
				end
			)

			return ports
		end
	}
}

local function parseInput()
	local parse = json.new()
	local done, err

	while true do
		local chunk = io.read(4096)
		if not chunk then
			break
		elseif not done and not err then
			done, err = parse:parse(chunk)
		end
	end

	if not done then
		print(json.stringify({ error = err or "Incomplete input" }))
		os.exit(1)
	end

	return parse:get()
end

local function validateArgs(func, uargs)
	local method = methods[func]
	if not method then
		print(json.stringify({ error = "Method not found" }))
		os.exit(1)
	end

	if type(uargs) ~= "table" then
		print(json.stringify({ error = "Invalid arguments" }))
		os.exit(1)
	end

	uargs.ubus_rpc_session = nil

	local k, v
	local margs = method.args or {}
	for k, v in pairs(uargs) do
		if margs[k] == nil or
		   (v ~= nil and type(v) ~= type(margs[k]))
		then
			print(json.stringify({ error = "Invalid arguments" }))
			os.exit(1)
		end
	end

	return method
end

if arg[1] == "list" then
	local _, method, rv = nil, nil, {}
	for _, method in pairs(methods) do rv[_] = method.args or {} end
	print((json.stringify(rv):gsub(":%[%]", ":{}")))
elseif arg[1] == "call" then
	local args = parseInput()
	local method = validateArgs(arg[2], args)
	local result, code = method.call(args)
	print((json.stringify(result):gsub("^%[%]$", "{}")))
	os.exit(code or 0)
end
